package za.org.jcicada.genss.sensor.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import za.org.jcicada.genss.ITerminateServiceListener;
import za.org.jcicada.genss.messages.Result;
import za.org.jcicada.genss.messages.SuccessCode;
import za.org.jcicada.genss.sensor.ISamplingStrategy;
import za.org.jcicada.genss.sensor.ISensor;
import za.org.jcicada.genss.sensor.ISensorObserver;
import za.org.jcicada.genss.sensor.ISensorSubject;
import za.org.jcicada.genss.sensor.ISensorValue;
import za.org.jcicada.genss.sensor.SensorStatus;
import za.org.jcicada.genss.sensor.SensorURN;
import za.org.jcicada.genss.sensor.ISamplingStrategy.IListener;

/**
 * Implementation of the ISensorSubject interface. It keeps track of which
 * observers are attached to which sensors and uses a SampleReactor to keep
 * track of the sampling strategies.
 * @author rcrida
 */
public class SensorSubject implements ISensorSubject {
    
    /**
     * Listener class is used as a callback from the sampling strategy to receive
     * sensor update events and pass them on to the observer.
     * @author rcrida
     */
    public class Listener implements IListener {
        
        /**
         * The observer to be informed of the sensor updates
         */
        private ISensorObserver observer;
        
        /**
         * Construct listener
         * @param observer to be informed of the sensor updates
         */
        public Listener(ISensorObserver observer) {
            this.observer = observer;
        }

        public void update(SensorURN sensorURN, Object value) {
        	if (value instanceof ISensorValue) {
        		observer.informSensor(sensorURN, (ISensorValue) value);
        	}
        	if (value instanceof SensorStatusValue) {
        		observer.informSensorStatus(sensorURN, (SensorStatusValue) value);
        	}
        }
    }
    
    /**
     * Object used for logging messages
     */
    private static final Log logger = LogFactory.getLog(SensorSubject.class);
    
    /**
     * Map containing observer to attached sensor URIs set
     */
    private Map<ISensorObserver, Set<SensorURN>> observerSensorMap = new HashMap<ISensorObserver, Set<SensorURN>>();
    
    /**
     * Map containing observer to associated listener object for use by sampling
     * strategies.
     */
    private Map<ISensorObserver, IListener> observerListenerMap = new HashMap<ISensorObserver, IListener>();
    
    /**
     * Set of all sensor URIs that this subject knows about.
     */
    private Set<SensorURN> sensorURNs = new HashSet<SensorURN>();
    
    /**
     * Map from sensorURI to actual sensor objects.
     */
    private Map<SensorURN, ISensor> sensorMap = new HashMap<SensorURN, ISensor>();
    
    /**
     * Sample reactor is primarily used to perform polling of sensors for periodic
     * sampling strategies but manages attachment between observers and the sensors
     * via their specified sampling strategies.
     */
    private SampleReactor reactor = new SampleReactor();
    
    /**
     * Listener that is called when sensors are terminated
     */
    private ITerminateServiceListener terminateListener;
    
    /**
     * Add a sensor with the specified URI to the subject
     * @param sensorURN the sensor URI
     * @param sensor the actual sensor object
     */
    public void addSensor(SensorURN sensorURN, ISensor sensor) {
        sensorURNs.add(sensorURN);
        sensorMap.put(sensorURN, sensor);
    }
    
    /**
     * Remove a sensor with the specified URN from the subject
     * @param sensorURN the sensor URN
     * @return true if successfully removed
     */
    public boolean removeSensor(SensorURN sensorURN) {
        if (sensorURNs.remove(sensorURN)) {
            ISensor sensor = sensorMap.remove(sensorURN);
            Set<ISensorObserver> observers = reactor.removeSensor(sensor);
            for (ISensorObserver observer: observers) {
                observer.informSensor(sensorURN, new SensorValue(sensor.getValue().getValue()));
                observer.informSensorStatus(sensorURN, new SensorStatusValue(SensorStatus.UNKNOWN));
                observer.informTerminateService(sensorURN);
                observerSensorMap.get(observer).remove(sensorURN);
                
            }
            return true;
        }
        return false;
    }
    
    /**
     * Set the terminate listener to be informed when sensors are destroyed.
     * @param listener 
     */
    public void setTerminateListener(ITerminateServiceListener listener) {
        this.terminateListener = listener;
    }
    
    /**
     * Get the sensor corresponding to the specified URI
     * @param sensorURN the desired sensor URI
     * @return sensor object corresponding to the specified URI or null if not present
     */
    public ISensor getSensor(SensorURN sensorURN) {
        return sensorMap.get(sensorURN);
    }
    
    public void requestAttach(ISensorObserver observer, Set<SensorURN> sensorURNs) {
        // Remove any invalid sensor IDs
        Set<SensorURN> filterURNs = new HashSet<SensorURN>(sensorURNs);
        filterURNs.retainAll(this.sensorURNs);
        // Add the mapping to observerSensorMap
        if (observerSensorMap.containsKey(observer)) {
            observerSensorMap.get(observer).addAll(filterURNs);
        }
        else {
            observerSensorMap.put(observer, filterURNs);
            // create a suitable listener for the observer
            observerListenerMap.put(observer, new Listener(observer));
        }
        // Send attached sensor types and values to observer
        for (SensorURN sensorURN: filterURNs) {
            ISensor sensor = sensorMap.get(sensorURN);
            observer.informSensorType(sensorURN, sensor);
            observer.informSensor(sensorURN, sensor.getValue());
            observer.informSensorStatus(sensorURN, sensor.getStatusValue());
        }
        // Now compose the reply
        Result result;
        if (filterURNs.equals(sensorURNs)) {
            result = new Result();
        }
        else {
            if (filterURNs.isEmpty()) {
                String message = "No valid sensorURIs specified";
                result = new Result(SuccessCode.COMPLETE_FAILURE, message);
                logger.error(message);
            }
            else {
                String message = "Not all sensorURIs specified were valid";
                result = new Result(SuccessCode.PARTIAL_SUCCESS, message);
                logger.error(message);
            }
        }
        observer.replyAttach(result, filterURNs);
    }

    public void requestDetach(ISensorObserver observer, Set<SensorURN> sensorURNs) {
        if (!observerSensorMap.containsKey(observer)) {
            String message = "Specified observer not currently attached";
            observer.replyDetach(new Result(SuccessCode.COMPLETE_FAILURE, message), null);
            logger.error(message);
            return;
        }
        // Remove any invalid sensor IDs
        Set<SensorURN> filterURNs = new HashSet<SensorURN>(sensorURNs);
        filterURNs.retainAll(observerSensorMap.get(observer));
        // Inform the observer of the current status
    	detachObserver(observer, filterURNs);
        // Remove sensors from observerSensorMap
        observerSensorMap.get(observer).removeAll(filterURNs);
        if (observerSensorMap.get(observer).size() == 0) {
            observerSensorMap.remove(observer);
        }
        // Now compose the reply
        Result result;
        if (filterURNs.equals(sensorURNs)) {
            result = new Result();
        }
        else {
            if (filterURNs.isEmpty()) {
                String message = "No valid sensorURIs specified";
                result = new Result(SuccessCode.COMPLETE_FAILURE, message);
                logger.error(message);
            }
            else {
                String message = "Not all sensorURIs specified were valid";
                result = new Result(SuccessCode.PARTIAL_SUCCESS, message);
                logger.error(message);
            }
        }
        observer.replyDetach(result, filterURNs);
    }

    public void requestSensorSet(ISensorObserver observer) {
        // Send all sensor types and values to observer
        for (SensorURN sensorURN: sensorURNs) {
            ISensor sensor = sensorMap.get(sensorURN);
            observer.informSensorType(sensorURN, sensor);
            observer.informSensor(sensorURN, sensor.getValue());
        }
        // Finally send the summary of what has been sent
        observer.replySensorSet(new Result(), sensorURNs.size());
    }

    public void requestSetSamplingStrategy(ISensorObserver observer, ISamplingStrategy strategy) {
        SensorURN sensorURN = strategy.getSensorURN();
        if (sensorURN == null) {
            String message = "Can't specify null sensorURI in sampling strategy";
            observer.replySetSamplingStrategy(new Result(SuccessCode.COMPLETE_FAILURE, message));
            logger.error(message);
            return;
        }
        ISensor sensor = sensorMap.get(sensorURN);
        if (sensor == null) {
            String message = String.format("Unrecognized sensorURI: %s", sensorURN);
            observer.replySetSamplingStrategy(new Result(SuccessCode.COMPLETE_FAILURE, message));
            logger.error(message);
            return;
        }
        Set<SensorURN> sensorURNs = observerSensorMap.get(observer);
        if (sensorURNs == null || !sensorURNs.contains(sensorURN)) {
            String message = String.format("This observer is not attached to %s", sensorURN.toString());
            observer.replySetSamplingStrategy(new Result(SuccessCode.COMPLETE_FAILURE, message));
            logger.error(message);
            return;
        }
        strategy.setListener(observerListenerMap.get(observer));
        reactor.addSensor(observer, strategy, sensor);
        observer.replySetSamplingStrategy(new Result());
    }
    
    public void informDisconnect(ISensorObserver observer) {
        detachAll(observer);
    }

    private void detachObserver(ISensorObserver observer, Set<SensorURN> sensorURNs) {
    	for (SensorURN sensorURN: sensorURNs) {
    		ISensor sensor = sensorMap.get(sensorURN);
    		reactor.detachSensor(observer, sensor);
    		observer.informSensor(sensorURN, new SensorValue(sensor.getValue().getValue()));
    		observer.informSensorStatus(sensorURN, new SensorStatusValue(SensorStatus.UNKNOWN));
    		observer.informTerminateService(sensorURN);
    	}
    	observer.informDisconnect();
    }
    
    /**
     * Detach observer from all sensors
     * @param observer to be detached from all sensors
     */
    public void detachAll(ISensorObserver observer) {
        Set<SensorURN> sensorURNs = getAttachedSensorURNs(observer);
        // Inform the observer of the current status
        if (observer != null && sensorURNs != null) {
        	detachObserver(observer, sensorURNs);
	        // Remove observer from observerSensorMap
	        observerSensorMap.remove(observer);
        }
    }

    /**
     * Stop the sensor subject. This stops the sampling reactor.
     */
    public void stop() {
        for (ISensorObserver observer: new ArrayList<ISensorObserver>(observerSensorMap.keySet())) {
        	detachAll(observer);
        }
        // terminate all of the sensors
        if (terminateListener != null) {
            for (SensorURN sensorURN: sensorURNs) {
                terminateListener.terminateService(sensorURN);
            }
        }
        reactor.stop();
    }
    
    /**
     * @return the number of strategies registered in the reactor.
     */
    public int getNumStrategies() {
        return reactor.getNumStrategies();
    }

    /**
     * Get the set of all sensors that a particular observer has attached to
     * @param observer the observer object
     * @return the set of sensor URIs that the observer is attached to or null
     *         if the observer is not attached to any sensors.
     */
    public Set<SensorURN> getAttachedSensorURNs(ISensorObserver observer) {
        if (!observerSensorMap.containsKey(observer)) {
            String message = "Observer not currently attached";
            logger.error(message);
            return null;
        }
        // Clone set so that it can't be accidentally modified
        return new HashSet<SensorURN>(observerSensorMap.get(observer));
    }
    
}
